#!/usr/bin/python
#-*-coding: utf-8 -*-

#This file is part of pyLot library.
#
# pyLot is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# pyLot is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with pyLot.  If not, see <http://www.gnu.org/licenses/>.

__author__ = u'Pierre Puiseux, Guillaume Baty'
__copyright__ = u"Copyright 2011-2012 (C) andheo, Université de Pau et des Pays de l'Adour"
__credits__ = [u'Pierre Puiseux', u'Guillaume Baty']
__license__ = "GNU Lesser General Public License"

__all__ = [
           u'Datum',
           u'DatumCastingException',
           u'value_to_pylot_str',
           u'datum_name',
           ]

import re
import sys
import cStringIO #used for CSV string handling
import warnings
import copy

try :
  import xml.etree.cElementTree as xml
except ImportError :
  import xml.etree.ElementTree as xml

from _pylot.minimal.path import Path, FilePath, DirPath
from _pylot.minimal.evaluators import eval_type, eval_literal

from _pylot.data import regexpr
from _pylot.data.fileformat import (DialectPyLotCSV, DialectPyLotList)

from _pylot.minimal.utils import (check_var_naming_convention, check_type,
  unicode2, list_to_unicode_list, uprint, error)
from _pylot.minimal.system import ENCODING, repr_unicode
from _pylot.minimal.csvtools import (UnicodeWriter, unicode_csv_reader)
from _pylot.minimal.datum_natures import StrList, Vector, PyLotList, BaseList, Coordinate

from datetime import datetime

TRUE_STRINGS = (u'yes', u'on', u'true', u'1', u'y')

class DatumNotAllowedOperation(Exception):
  pass

class DatumCastingException(Exception):
  def __init__ (self, name, nature, value):
    check_type(u'name', name, unicode)
    check_type(u'nature', nature, type, none=True)

    self.name = name
    self.nature = nature
    self.value = value

  def __unicode__ (self):
    return  u"E013 - %(NAME)s: cannot convert %(VALUE)r to %(NATURE)s" % dict(
      NAME=self.name, VALUE=self.value, NATURE=self.nature.__name__)

  def __str__ (self):
    return unicode(self).encode(ENCODING)

edit_error = u'You must use Datum methods to edit value_list'

class DatumValueList(list):

  def __init__(self, datum, lst=[]):
    self._datum = datum
    list.__init__(self)

    if self._datum.nature is None :
      raise TypeError, 'E011 - nature must be defined'
    for val in lst :
      self.reallyAppend(val)

  def __add__(self, value_list):
    raise NotImplementedError

  def append(self, value):
    raise DatumNotAllowedOperation, edit_error

  def remove(self, value):
    raise DatumNotAllowedOperation, edit_error

  def reallyAppend(self, value):
    try :
      nature, value = self._datum.arrangeValue(value, self._datum._nature)
    except :
      pass
    else :
      if value in self or value is None :
        pass
      else :
        super(DatumValueList, self).append(value)

  def reallyRemove(self, value):
    if value in self :
      super(DatumValueList, self).remove(value)


def datum_name (datum) :
  check_type(u'datum', datum, Datum)
  if datum.alias :
    return datum.alias
  else :
    return datum.name.capitalize()

class Datum(object):
  u"""
  The Datum class provides a generic container for simple python types.

  Datum provides :

    - meta-informations: group, alias, tooltip
    - type information: nature
    - default and available values
    - auto casting
    - convenient methods : upFromDict, toCsv, toDict, ...

  Type casting :

  Mainly identical to python casting except :

    - bool : recognize 'on', 'off', 0, 1, 'true', 'false' (no case sensitive)
    - Path : expand '~' and environment variables

  """
  ordered_keys = (u'name', u'nature', u'value', u'default_value',
                  u'value_list', u'tooltip', u'group', u'alias')
  """ ordered tuple of available fields"""

  csv_order = u';'.join(ordered_keys)

  valid_nature = (int, unicode, float, bool, Path, FilePath, DirPath,
                  StrList, PyLotList, Vector, Coordinate)
  deprecated_nature = ()
  """ tuple of natures supported by Datum """


  ModeAutoUpdate = u'AutoUpdate'
  ModeDoNotUpdate = u'DoNotUpdate'

  epsilon = 1e-12

  def __init__(self, name=None, nature=None, value=None, default_value=None,
               value_list=None, tooltip=None, group=None, alias=None, *t, **d):
    u"""
    >>> print Datum(name=u'Huu',value=1, nature=int, alias=u'Hooo')
    Datum(u'Huu', int, 1)
    >>> print Datum(u'Huu', int, 1)
    Datum(u'Huu', int, 1)

    Path handling :

    >>> Datum(u'p', Path, u'/')
    Datum(u'p', Path, Path(u'/'), None, [Path(u'/')], None, None, None)

    .. seealso ::

      :ref:`specification-Datum`

    """
    self._signal_methods = dict(
      valueChanged=[],
      defaultValueChanged=[],
      valueListChanged=[],
      natureChanged=[],
      toolTipChanged=[],
      )

    check_type(u'name', name, [Datum, unicode], none=True)
    self.setUpdateMode(Datum.ModeAutoUpdate)

    for attr in self.ordered_keys:
      setattr(self, '_%s' % attr, None)

    self.clear()
    self._creation_date = datetime.now()
    lst = [name, nature, value, default_value, value_list, tooltip, group, alias]
    lst += t
    final_dict = self.listToDict(lst)
    final_dict.update(d)
    self.upFromDict(final_dict)

  def _emitSignal(self, signal, *args, **kargs):
    for meth in self._signal_methods[signal] :
      meth(*args, **kargs)

  def connect(self, signal, method):
    # use loop on all elements instead of
    # "if method not in self._signal_objs[signal]: self._signal_methods[signal].append(method)
    # because this instruction raises 
    # NotImplementedError: operator not implemented.
    # on some system (at least mageia 1, Python 2.7.1 (r271:86832, Sep  5 2011, 14:50:51), PySide)
    IN_DICT = False
    for existing in self._signal_methods[signal] :
      if id(method) == id(existing):
	IN_DICT = True
	break
    if IN_DICT is False :
      self._signal_methods[signal].append(method)

  def disconnect(self, signal, method):
    if method in self._signal_methods[signal] :
      self._signal_methods[signal].remove(method)

  def clear(self):
    for attr in self.ordered_keys:
      setattr(self, attr, None)

  def update(self, d, mod_attrs=None):
    """
    Updates datum from dict. Modifies only fields specified in mod_attrs.

    :param d: a field=value dict
    :type d: :obj:`dict`
    :param mod_attrs: tuple of attributes (fields) you want to modify
    :type mod_attrs: :obj:`tuple`
    """
    check_type(u'd', d, [self.__class__, dict])
    if isinstance(d, self.__class__) :
      d = d.toDict()
    self.upFromDict(d, mod_attrs=mod_attrs)

  def userUpgrade(self, config_option):
    u"""
    >>> d = Datum(u'd', int, 1, alias=u'd')
    >>> up = Datum(u'd', unicode, u'3', alias=u'tmp')
    >>> d.userUpgrade(up)
    >>> d
    Datum(u'd', int, 3, None, [1, 3], None, None, u'd')
    """
    self.update(config_option,
                mod_attrs=[u'value', u'default_value', u'value_list'])

  def upFromPyLot (self, line=None):
    u"""
    >>> d = Datum()
    >>> d.upFromPyLot(u'dat a,b,c')
    >>> print d
    Datum(u'dat', PyLotList, PyLotList([u'a', u'b', u'c']))
    >>> d = Datum(u'dat', bool)
    >>> d.upFromPyLot(u'bool False')
    >>> print d
    Datum(u'bool', bool, False)
    """
    if line :
      line = line.strip()
      if line and not regexpr.co_comment.match(line) :
        dic = {}
        variables = line.split()
        name = variables[0]
        values = line[len(name):].strip()
        if u',' not in values :
          dic[u'name'] = name
          if self.nature is None :
            dic[u'nature'] = unicode
          dic[u'value'] = values.strip()
        else :
          dic[u'name'] = name
          if self.nature is None :
            dic[u'nature'] = PyLotList
          dic[u'value'] = values.split(u',')
        self.upFromDict(dic)

  def upFromCsv (self, line, mod_attrs=None):
    lst = self.csvToList(line)
    dic = self.listToDict(lst)
    self.upFromDict(dic, mod_attrs)

  def upFromDict (self, d, mod_attrs=None):#d=dictionnaire
    u"""
    :param d: a datum attribute dict
    :type d: :obj:`dict`

    :param mod_attrs: tuple of writable attributes. Default : all
    :type mod_attrs: :obj:`tuple`
    """
    check_type(u'mod_attrs', mod_attrs, [list, tuple], none=True)
    if mod_attrs is None :
      mod_attrs = self.ordered_keys

    for attr in mod_attrs:
      if attr in d:
        if attr == u'value_list' :
          self.upValueList(d[attr])
        else :
          setattr(self, attr, d[attr])

  @classmethod
  def FromCsv (cls, src):
    datum = cls()
    datum.upFromCsv(src)
    return datum

  @classmethod
  def FromPyLot (cls, src):
    datum = cls()
    datum.upFromPyLot(src)
    return datum

  def convert(self, val, nat):
    return self.staticConvert(val, nat)

  @classmethod
  def staticConvert(cls, val, nat):
    u"""
    Casting method.
    Convert value 'val' to type 'nature'

    :param val: the value you want to convert
    :type val: all types supported by Datum
    :param nat: a valid Datum nature
    :type nat: :obj:`type`
    """
    if val is None and nat is unicode :
      return None
    elif nat == bool and isinstance(val, str) :
      val = val.decode(ENCODING)
      if val.lower() in TRUE_STRINGS:
        return True
      else:
        return False
    elif nat == bool and isinstance(val, unicode) :
      if val.lower() in TRUE_STRINGS:
        return True
      else:
        return False
    elif issubclass(nat, Path) :
      if val and isinstance(val, (unicode, Path)) :
        if val[0] == u'~' : return nat(val).expanduser().expandvars()
        else : return nat(val).expandvars()
      elif val :
        return nat(val).expanduser().expandvars()
      else : return None
    elif issubclass(nat, float):
      if val == 0 : return 0. # change -0. to 0.
      else : return nat(val)
    elif nat == list and isinstance(val, (set, list, tuple)):
      return list_to_unicode_list(list(val))
    elif nat == list and not isinstance(val, (str, unicode)) :
      return list_to_unicode_list([val])
    elif nat == list :
      return list_to_unicode_list(list(eval_literal(val)))
    else :
      return nat(val)

  @classmethod
  def convertValueFromCsv(cls, val, nat):
    try :
      return nat.FromCsv(val)
    except AttributeError :
      return cls.staticConvert(val, nat)

  @classmethod
  def convertValueFromPyLot(cls, val, nat):
    try :
      return nat.FromPyLot(val)
    except AttributeError :
      return cls.staticConvert(val, nat)

  def arrangeValue(self, val, nat):
    u"""
    :param val: value
    :type val: type supported by datum else an exception is raised

    :param nat: nature. Must be a python type supported by Datum or None
    :type nat: :obj:`type` or :obj:`None`
    """
    if nat is not None and val is not None:
      val = self.convert(val, nat)
    elif nat is not None and val is None : pass
    elif nat is None and val is None : pass
    elif nat is None and val is not None :
      nat = type(val)
      self.checkNature(nat)
    return (nat, val)

  def checkNature(self, nat):
    if nat is None or nat in self.valid_nature :
      return
    elif nat in self.deprecated_nature :
      warnings.warn('Deprecated since 2010-04-02, use PyLotList, StrList or Vector instead of SimpleStringList or StringList', DeprecationWarning)
      return
    else:
      raise TypeError, error(tr_(u"E011 - Invalid Datum's nature %r") % nat.__name__)

  def toDict (self, mode=u'light'):
    u"""
    Returns a dict representation of current Datum.

    >>> d = Datum(u'd', int, 1)
    >>> d.toDict()
    {u'value_list': [1], u'name': u'd', u'value': 1, u'nature': <type 'int'>}
    """
    dic = {}
    for attr in self.ordered_keys :
      val = getattr(self, attr)
      if val is not None or val is None and mode == u'full' :
        dic[attr] = val
    return dic

  def toList (self):
    u"""
    Returns a list representation of current Datum

    >>> d = Datum(u'a', int, 1)
    >>> d.toList()
    [u'a', <type 'int'>, 1, None, [1], None, None, None]
    """
    l = []
    for attr in self.ordered_keys :
      l.append(getattr(self, attr))
    return l

  def toPyLot (self):
    u"""
    >>> d = Datum(u'd', int, 1, 1, value_list=[1,4,5])
    >>> d.toPyLot()
    u'd 1'
    """
#    value_list = copy.copy(self.value_list)
    try :
      value = self.value.toPyLotStr()
    except AttributeError :
      if issubclass(self.nature, (list, tuple)) :
        value = ','.join([unicode(val) for val in self.value])
      else :
        value = self.value
      line = u'%s %s' % (self.name, value)
      return line.strip()
    else :
      return value

  def toCsv (self):
    u"""
    Exports datum to CSV string. All characters, including CSV delimiters are
    allowed in strings.

    >>> print Datum(u'd', int, 1, 2, [3,4], tooltip=u"yes", group=u'grooooup').toCsv()
    d;int;1;2;[1,2,3,4];yes;grooooup;

    >>> tt = u"'tooltip' with ; and , 'utf8' : 'æ ð' "
    >>> print Datum(u'd', int, 1, 2, [3,4], tooltip=tt).toCsv()
    d;int;1;2;[1,2,3,4];"'tooltip' with ; and , 'utf8' : 'æ ð' ";;

    >>> print Datum().toCsv()
    ;;;;;;;

    :return: CSV string
    :rtype: :obj:`unicode`
    """
    output = cStringIO.StringIO()
    writer = UnicodeWriter(output, dialect=DialectPyLotCSV())
    writer.writerow(self.toCsvList())
    csvstr = output.getvalue().decode(u'utf-8')
    output.close()
    return csvstr.strip()

  def listToDict (self, src, mode=u'light'):
    u"""
    Transforms an ordered list (according ordered_keys order) to dictionary
    If l is None, return a dict representation of Datum

    >>> d = Datum()
    >>> print d.listToDict([u'AAA', int, 1, 2,[1,4,67]])
    {u'default_value': 2, u'value_list': [1, 4, 67], u'name': u'AAA', u'value': 1, u'nature': <type 'int'>}

    :param l: attributes' list or None
    :type l: :obj:`list` or :obj:`None`
    """
    dic = {}
    for i, attr in enumerate(self.ordered_keys):
      try: val = src[i]
      except IndexError :
        break #yen a pas assez dans la liste
      else:
        if val is not None or val is None and mode == u'full'  :
          dic[attr] = val
    return dic

  @classmethod
  def findTypedStrings (cls, line, nature) :
    u"""
    A typed string is a string like "nature(params)".
    This function finds all typed strings and return a list of found values
    """
    vals = []
    string = line
    co = re.compile(u'%s[\s]*\([\s]*' % nature)
    co2 = re.compile(u'%s[\s]*' % nature)
    res = co.search(string)
    while res :
      res2 = co2.search(string)
      string = string[res2.end():]
      par = 0
      i = 0
      for c in string :
        i += 1
        if c == u'(' : par += 1
        elif c == u')' : par -= 1
        if par == 0 : break
      res = co.search(string)
      val = string[:i]
      if val :
        vals.append(val)
    return vals

  @classmethod
  def _formatpyLotCSVValueList(cls, strlist):
    u"""
    >>> csvlist = u'[1, 2],[3, 4]'
    >>> Datum._formatpyLotCSVValueList(csvlist)
    [u'[1, 2]', u'[3, 4]']
    >>> csvlist = u'[[1, 2],[3, 4]],[a, b]'
    >>> Datum._formatpyLotCSVValueList(csvlist)
    [u'[[1, 2],[3, 4]]', u'[a, b]']
    """
    quotechar = u"'"
    new_list = []
    list_level = 0
    quoting = 0
    var = u''
    for c in strlist :
      if c == u'[' and quoting % 2 == 0 :
        list_level += 1
      elif c == u']' and quoting % 2 == 0 :
        list_level -= 1
        if list_level == 0 :
          var += c
      elif c == quotechar :
        quoting += 1
      if list_level > 0 :
        var += c
      else :
        if var.strip():
          new_list.append(var)
        var = u''

    return new_list

  def csvToList(self, src=None):
    u"""
    Makes a list from CSV string

    >>> d = Datum(u'a', int, 1)
    >>> d.csvToList(u'a;int;1;2;[3,4]')
    [u'a', <type 'int'>, 1, 2, [3, 4], None, None, None]

    >>> d = Datum(tooltip=u'<-- "special char : ; !" -->')
    >>> lst = d.toCsv()
    >>> print d.csvToList(lst)[5]
    <-- "special char : ; !" -->
    """
    check_type(u'src', src, unicode)

    # l = [None, None, None, ...]
    l = [None for attr in self.ordered_keys]

    # transform csv line to list (;int;3 -> [None, 'int', 3, ...])
    reader = unicode_csv_reader([src], dialect=DialectPyLotCSV())
    for row in reader :
      csvl = [field.strip() for field in row]

    for i in range(min(len(csvl), len(l))):
      val = csvl[i]
      if val :
        l[i] = val

    # transform str type into python type ('int' -> <type 'int'>)
    l[1] = self.getType(l[1])

    if l[1] in self.deprecated_nature :
      uprint(u'Deprecated since 2010-04-02, use PyLotList, StrList or Vector instead of SimpleStringList or StringList',
             file=sys.stderr)
      uprint(u'Datum/Parameter %s ignored' % l[0], file=sys.stderr)
    elif l[1] in self.valid_nature :
      #convert values to new type
      if l[2] or l[1] == unicode :
        l[2] = self.convertValueFromCsv(l[2], l[1]) #convert value
      if l[3] or l[1] == unicode :
        l[3] = self.convertValueFromCsv(l[3], l[1]) #convert default_value

      # transform 5th value (value_list) to list
      # 5th element is like a csv separated by ','
      if l[4] :
        # remove optionnal '[]' delimiters
        # exemple: u"[[1,2],[3.0,4.5]]" -> u"[1,2],[3.0,4.5]"
        str_list = l[4].strip()
        if str_list[0] == u'[' and str_list[-1] == u']' :
          str_list = str_list[1:-1]

        l[4] = []
        if issubclass(l[1], (list, BaseList)) :
          csvlst = self._formatpyLotCSVValueList(str_list)
        else :
          lst_reader = unicode_csv_reader([str_list],
            dialect=DialectPyLotList())
          for row in lst_reader :
            csvlst = [field.strip() for field in row]

        for val in csvlst :
          if val :
            val = self.convertValueFromCsv(val, l[1])
            l[4].append(val)
    else :
      # if nature isn't defined in str, values cannot be converted so ...
      l[2] = l[3] = None

    return l

  def upValueList (self, value_list):
    if self._value_list is None :
      self.value_list = DatumValueList(self, value_list)
    else :
      FLAG_CHANGED = False
      for value in value_list :
        if value not in self._value_list :
          FLAG_CHANGED = True
          self._value_list.reallyAppend(value)
      if FLAG_CHANGED :
        self._emitSignal('valueListChanged', self, self._value_list)

  def removeValue(self, value):
    if self.value_list and value in self._value_list:
      self.value_list.reallyRemove(value)
      self._emitSignal('valueListChanged', self, self._value_list)

  def addValue(self, value):
    if self.value_list is None :
      self.value_list = DatumValueList(self, [value])
    else :
      self.value_list.reallyAppend(value)
      self._emitSignal('valueListChanged', self, self._value_list)

  def doNotUpValueList(self, value_list):
    pass

  def setUpdateMode(self, mode):
    if mode == self.ModeDoNotUpdate :
      self._autoUpValueList = self.doNotUpValueList
    else :
      self._autoUpValueList = self.upValueList


  @classmethod
  def _formatvalue (cls, value, light=False, repr_func=repr):
    u"""
    3.0000000001 -> '3.0'
    'str' -> "'str'"
    Path('/') -> "Path('/')"
    """
    if type(value) in (Path,) :
      if light is True :
        return unicode(value)
      else :
        return repr_func(value).decode(ENCODING)
    elif type(value) in (float, complex) :
      return unicode(value)
    elif type(value) in (unicode,) :
      return repr_func(value).decode(ENCODING)
    else :
      if light : return unicode(value)
      else : return repr_func(value).decode(ENCODING)

  @classmethod
  def valuetoCsvValue (cls, value):
    u"""
    3.0000000001 -> '3.0'
    'str' -> "'str'"
    Path('/') -> "Path('/')"
    """
    try :
      return value.toCsv()
    except AttributeError :
      if isinstance(value, Path) :
        return unicode(value)
      elif isinstance(value, (float, complex)) :
        return unicode(value)
      elif isinstance(value, unicode) :
        return value
      else :
        return unicode(value)

  def valueToXmlElement (self, value, role=None):
    try :
      node = value.toXmlElement(role=role)
    except AttributeError :
      node = xml.Element('value')
      if role :
        node.attrib['role'] = role
      node.text = unicode(value)
    return node

  def _reprList (self, repr_func=repr):
    u"""
    .. warning ::
      _reprList return :obj:`str` list, not :obj:`unicode` list
    """
    try :
      nature = self._nature.__name__
    except AttributeError :
      nature = u'None'

    if self.value_list is None : value_list = u'None'
    else :
      vlist = [self._formatvalue(elem).encode(ENCODING) for elem in self.value_list]
      value_list = '[%s]' % ', '.join(vlist)

    str_list = [
    repr(self.name),
    nature,
    self._formatvalue(self.value, repr_func=repr_func).encode(ENCODING),
    self._formatvalue(self.default_value, repr_func=repr_func).encode(ENCODING),
    value_list,
    ]

    for i in range(5, len(self.ordered_keys)) :
      attr = self.ordered_keys[i]
      val = getattr(self, attr)
      if val is None :
        str_list.append(str(None))
      elif isinstance(val, (unicode, int, float)) :
        str_list.append(self._formatvalue(val, repr_func=repr_func).encode(ENCODING))
      else :
        str_list.append('u%s' % repr(str(val)))

    return str_list

  def toCsvList (self):
    try : nature = self._nature.__name__
    except AttributeError : nature = u''

    if nature == u'unicode' :
      nature = u'str'
    elif nature in (u'SimpleStringList', u'StringList', u'list') :
      warnings.warn('Deprecated since 2010-04-02, use PyLotList, StrList or Vector instead of SimpleStringList or StringList', DeprecationWarning)
      nature = u'PyLotList'

    if not self._value_list :
      value_list = u''
    else :
      vlist = [self.valuetoCsvValue(elem) for elem in self._value_list]
      value_list = u'[%s]' % u','.join(vlist)

    if self.name is None:
      name = u''
    else:
      name = self.name
    if self.value is None:
      value = u''
    else:
      value = self.valuetoCsvValue(self.value)
    if self.default_value is None:
      default_value = u''
    else:
      default_value = self.valuetoCsvValue(self.default_value)

    str_list = [
    name,
    nature,
    value,
    default_value,
    value_list,
    ]

    for i in range(5, len(self.ordered_keys)) :
      attr = self.ordered_keys[i]
      str_list.append(unicode2(getattr(self, attr)))

    return str_list


  def valueChanges(self, old, new):
#    print self.name, old, new,
    if old is None and new is None :
      changed = False
    elif self._nature in (float,) :
      if old is None and new is not None :
        changed = True
      elif new is None and old is not None :
        changed = True
      elif abs(new - old) > self.epsilon:
        changed = True
      else :
        changed = False
    elif old != new :
      changed = True
    else :
      changed = False
    return changed

  def __unicode__(self):
    txt = ', '.join(self._reprList(repr_func=repr_unicode)[:3])
    txt = "%s(%s)" % (self.__class__.__name__, txt)
    return txt.decode(ENCODING)

  def __str__ (self):
    return unicode(self).encode(ENCODING)

  def __repr__(self):
    lst = ', '.join(self._reprList(repr_func=repr))
    return "%s(%s)" % (type(self).__name__, lst.encode(ENCODING))

  def _unicode_repr_(self):
    u"""
    Like repr but encode non ASCII characters using default encoding
    """
    lst = ', '.join(self._reprList(repr_func=repr_unicode))
    return "%s(%s)" % (type(self).__name__, lst)

  def __nonzero__(self):
    u"""
    A datum is considered False if one of name or nature isn't defined.
    "Not defined" doesn't mean "False"

    >>> bool(Datum())
    False
    >>> bool(Datum(None))
    False
    >>> bool(Datum(u'obj', None))
    False

    Though bool(0) or bool('') are False, the Datum is considered non zero
    because 0 or "" may be wanted values

    >>> bool(Datum(u'obj', value=0))
    True
    """
    return not ((self._name is None) or (self._nature is None))

  def __eq__ (self, other):
    u"""
    >>> d1 = Datum(u'a', int, 1, 0, [3,4], u'tt', u'gr', u'al')
    >>> d2 = Datum(u'a', int, 1, 0, [3,4], u'tt', u'gr', u'al')

    >>> d1 == d2
    True
    >>> d1.group = u'g'
    >>> d1 == d2
    False
    """
    if (type(other) != type(self)) :
      return False
    return self.toDict() == other.toDict()

  def __ne__ (self, other):
    return not self.__eq__(other)

  def __deepcopy__ (self, memo=None, _nil=[]):
    u"""
    >>> d = Datum(u'a', int, 1, 0, [3,4], u'tt', u'gr', u'al')
    >>> c = d
    >>> print c == d , c is d
    True True
    >>> import copy
    >>> c = copy.deepcopy(d)
    >>> print c == d , c is d
    True False
    >>> c.value_list = [888, 999]
    >>> c.value_list ; d.value_list
    [888, 999]
    [0, 1, 3, 4]
    """

    # TODO: Python3: remove lines from here ...
    new_dict = {}
    for key, value in self.toDict().items() :
      new_dict[key.encode(ENCODING)] = value
    # ... to here

    newdatum = self.__class__(**new_dict)
    return newdatum

  def copy(self):
    return self.__deepcopy__()

  @classmethod
  def getFieldsAll (cls):
    return [key for key in cls.ordered_keys]

  @classmethod
  def getFieldsStd (cls):
    lst = [key for key in cls.ordered_keys]
    lst.remove(u'name')
    lst.remove(u'nature')
    return lst

  @classmethod
  def getFieldsDesc (cls):
    return [u'tooltip', u'group', u'alias']

  def _getname (self):
    return self._name

  def _setname(self, name):
    u"""
    checks if name complies with python namming conventions
    """
    if name is None :
      self._name = None
    else :
      name = name.strip()
      check_type(u'name', name, unicode)
      check_var_naming_convention(name)
      self._name = name

  def _getnature(self):
    return self._nature

  @classmethod
  def getType (cls, T):
    if type(T) == type or T is None :
      return T
    elif T == u'None':
      return None
    elif type(T) == unicode :
      if T == u'str' :
        return unicode
      else :
        try :
          nature = eval_type(T, cls.valid_nature)
        except (NameError, ValueError) :
          raise TypeError, error(tr_(u"E011 - Invalid Datum's nature '%s'") % T)
        else :
          return nature
    else :
      raise TypeError, error(tr_(u"'%s' is not a valid nature") % T)

  def _setnature(self, T):
    u"""
    >>> r = Datum(u'd', int, 1)
    >>> r.nature = bool
    >>> print r
    Datum(u'd', bool, True)
    >>> r = Datum(u'd', unicode, u'a')
    >>> r.nature = int
    Traceback (most recent call last):
    ...
    DatumCastingException: E013 - value: cannot convert u'a' to int
    >>> print r
    Datum(u'd', unicode, u'a')
    """
    old_value = self.nature
    nature = self.getType(T)
    if nature is None :
      self._nature = None
      self.value = None
      self.default_value = None
      self.value_list = None
    else :
      self.checkNature(nature)
      CAN_CONVERT = True
      try :
        nat, value = self.arrangeValue(self.value, nature)
      except ValueError :
        CAN_CONVERT = False
        raise DatumCastingException(u'value', nature, self.value)

      try :
        nat, default_value = self.arrangeValue(self.default_value, nature)
      except ValueError :
        CAN_CONVERT = False
        raise DatumCastingException(u'default_value', nature, self.default_value)

      if CAN_CONVERT :
        self._nature = nature
        self.value = value
        self.default_value = default_value
        self.value_list = self.value_list
        if old_value != self._nature:
          self._emitSignal('natureChanged', self, old_value, self._nature)

  def _getvalue(self):
    return self._value

  def _setvalue(self, value):
    u"""
    value :
      1. set : self._value=value
      2. and append value to  self._value_list

    >>> d=Datum(u'D',int,1)
    >>> d.value_list,d.value
    ([1], 1)
    >>> d.value=372
    >>> d.value_list
    [1, 372]

    .. seealso ::

      :meth:`~_pylot.minimal.Datum.arrangeValue`
    """
    old_value = self._value
    self._nature, self._value = self.arrangeValue(value, self._nature)
    if self._value is not None :
      self._autoUpValueList([self._value])
    if self.valueChanges(old_value, self._value) :
      self._emitSignal('valueChanged', self, old_value, self._value)

  def _getdefault_value(self):
    return self._default_value

  def _setdefault_value(self, value):
    old_value = self.default_value
    self._nature, self._default_value = self.arrangeValue(value, self._nature)
    if self._default_value is not None :
      self._autoUpValueList([self._default_value])
    if self.valueChanges(old_value, self._value) :
      self._emitSignal('defaultValueChanged',
                       self, old_value, self._default_value)

  def _getvalue_list(self):
    if self._value_list is None :
      return None
    else :
      self._value_list.sort()
      return self._value_list

  def _setvalue_list(self, value_list):
    u"""
    Replaces value list

    >>> d = Datum(u"ADRESS",Path, u'/home')
    >>> d.value_list = [u'/home/puiseux', u'/etc', u'/var']
    >>> d.value_list
    [Path(u'/etc'), Path(u'/home/puiseux'), Path(u'/var')]
    """
    check_type(u'value_list', value_list, list, none=True)
    if value_list is None:
      self._value_list = None
      return

    self._value_list = DatumValueList(self, value_list)

    for val in value_list :
      try :
        val = self.convert(val, self.nature)
      except ValueError :
        pass
      else :
        if val not in self._value_list :
          self._value_list.reallyAppend(val)

    self._emitSignal('valueListChanged', self, self._value_list)

  def _gettooltip(self):
    return self._tooltip

  def _settooltip(self, tooltip):
    old_value = self._tooltip
    check_type(u'tooltip', tooltip, unicode, none=True)
    self._tooltip = tooltip
    if old_value != self._tooltip:
      self._emitSignal('toolTipChanged', self, old_value, self._tooltip)

  def _getdate(self):
    return self._date

  def _setdate(self, d):#date=[yyyy,mm,dd,]
    u"""date"""
    from datetime import date
    if type(d) == list:
      try : self._date = date(d[0], d[1], d[2])
      except IndexError:
        uprint(tr_(u"ValueError in Datum._setdate, date must be [yyyy,mm,dd]"))
    elif type(d) == date:
      self._date = d

  def _getcreation_date(self):
    u"""Cannot be changed"""
    return self._creation_date

  def _getalias(self):
    return self._alias

  def _setalias(self, alias):
    check_type(u'alias', alias, unicode, none=True)
    self._alias = alias

  def _getgroup(self):
    return self._group

  def _setgroup(self, group):
    if group is None :
      self._group = None
    else :
      group = group.strip()
      check_type(u'group', group, unicode)
      check_var_naming_convention(group)
      self._group = group

  # Datum's fields
  name = property(fget=_getname, fset=_setname, doc=u'Datum name (identifier)')
  nature = property(fget=_getnature, fset=_setnature, doc=u'Datum nature')
  value = property(fget=_getvalue, fset=_setvalue, doc=u'Datum value')
  default_value = property(fget=_getdefault_value, fset=_setdefault_value,
                           doc=u'Default value')
  value_list = property(fget=_getvalue_list, fset=_setvalue_list,
                         doc=u'Sample values')
  tooltip = property(fget=_gettooltip, fset=_settooltip, doc=u'Tooltip')
  group = property(fget=_getgroup, fset=_setgroup, doc=u'Group')
  alias = property(fget=_getalias, fset=_setalias,
                   doc=u'alias, usually="human readable" name of Datum')

  # date/time properties
  date = property(fget=_getdate, fset=_setdate, doc=u'Date')
  creation_date = property(fget=_getcreation_date,
                           doc=u'Creation date, cannot be changed')

def value_to_pylot_str (value):
  try :
    val = value.toPyLot()
  except AttributeError :
    val = unicode(value)
  return val

def datum_repr(datum):
  u"""
  >>> from pyLot.core import Datum
  >>> d = Datum(u'a', alias=u'alias')
  >>> print datum_repr(d)
  Datum(u'a', alias=u'alias')
  """
  default = datum.__class__()
  lst = datum._reprList(repr_func=repr)
  flst = []
  args = -1
  for i, attr in enumerate(datum.ordered_keys):
    if getattr(datum, attr) != getattr(default, attr) :
      args += 1
      if args == i :
        flst.append(lst[i])
      else:
        flst.append(u'%s=%s' % (attr, lst[i]))
  lst = u', '.join(flst)
  return u"%s(%s)" % (unicode(type(datum).__name__), lst)

if __name__ == u'__main__':
  import doctest
  doctest.testmod()

